package de.joergviola.mobj.client.repl;

import java.util.List;

import com.google.gwt.user.client.rpc.AsyncCallback;

import de.joergviola.mobj.client.ITransaction;
import de.joergviola.mobj.client.remote.RemoteTransactionAsync;
import de.joergviola.mobj.shared.Entity;
import de.joergviola.mobj.shared.ObjectNotFoundException;
import de.joergviola.mobj.shared.Persistable;
import de.joergviola.mobj.shared.Query;
import de.joergviola.mobj.shared.Schema;

public class ReplicatingTransaction implements ITransaction {

	public class HandleFailingOnlyCallback<T> implements AsyncCallback<T> {

		private final AsyncCallback<T> callback;

		public HandleFailingOnlyCallback(AsyncCallback<T> callback) {
			this.callback = callback;
		}

		@Override
		public void onFailure(Throwable caught) {
			callback.onFailure(caught);
		}

		@Override
		public void onSuccess(T result) {
		}

	}

	public class NullCallback<T> implements AsyncCallback<T> {

		@Override
		public void onFailure(Throwable caught) {
		}

		@Override
		public void onSuccess(T result) {
		}

	}

	public abstract class CallbackProxy<R> implements AsyncCallback<R> {

		private final AsyncCallback<R> callback;

		public CallbackProxy(AsyncCallback<R> callback) {
			this.callback = callback;
		}

		@Override
		public void onFailure(Throwable caught) {
			callback.onFailure(caught);
		}

		@Override
		public void onSuccess(R result) {
			afterSuccess(result);
			callback.onSuccess(result);
		}

		protected abstract void afterSuccess(R result);
	}

	private final ITransaction local;
	private final RemoteTransactionAsync remote;
	private final Schema schema;

	public ReplicatingTransaction(Schema schema, ITransaction local,
			RemoteTransactionAsync remote) {
		this.schema = schema;
		this.local = local;
		this.remote = remote;
	}

	private boolean remoteAvailable() {
		return remote != null;
	}

	private boolean localAvailable() {
		return local != null;
	}

	@Override
	public <T extends Persistable> void insert(T t, AsyncCallback<T> callback) {
		if (remoteAvailable()) {
			callback = decorateWithPostInsert(callback);
			remote.insert(t, callback);
		} else if (localAvailable()) {
			local.insert(t, callback);
		} else {
			// DBLog log = new DBLog(entity.getEntityName(), entity.getId(t),
			// INSERT);
			// local.insert(log, new NullCallback<DBLog>());
		}
	}

	private <T extends Persistable> void localInsertOrUpdate(final T result,
			final AsyncCallback<T> callback) {
		Entity<T> entity = schema.getEntity(result);
		Long id = (Long) entity.getId(result);
		if (id != null) {
			local.get(result.getClass().getName(), id, new AsyncCallback<T>() {

				@Override
				public void onFailure(Throwable caught) {
					if (caught instanceof ObjectNotFoundException) {
						local.insert(result, new HandleFailingOnlyCallback<T>(
								callback));
					} else
						callback.onFailure(caught);

				}

				@Override
				public void onSuccess(T localT) {
					local.update(result, new NullCallback<Void>());
				}
			});
		}
	}

	private <T extends Persistable> AsyncCallback<T> decorateWithPostInsert(
			final AsyncCallback<T> callback) {
		AsyncCallback<T> modified = callback;
		if (localAvailable()) {
			modified = new CallbackProxy<T>(callback) {
				@Override
				protected void afterSuccess(final T result) {
					localInsertOrUpdate(result, callback);
				}

			};
		}
		return modified;
	}

	@Override
	public <T extends Persistable> void update(T t, AsyncCallback<Void> callback) {
		if (remoteAvailable()) {
			callback = decorateWithPostUpdate(callback, t);
			remote.update(t, callback);
		} else if (localAvailable()) {
			local.update(t, callback);
		} else {
			// DBLog log = new DBLog(entity.getEntityName(), entity.getId(t),
			// UPDATE);
			// local.insert(log, new NullCallback<DBLog>());
		}
	}

	private <T extends Persistable> AsyncCallback<Void> decorateWithPostUpdate(
			AsyncCallback<Void> callback, final T t) {
		if (localAvailable()) {
			callback = new CallbackProxy<Void>(callback) {
				@Override
				protected void afterSuccess(Void result) {
					local.update(t, new NullCallback<Void>());
				}
			};
		}
		return callback;
	}

	@Override
	public <T extends Persistable> void get(String className, Long id,
			AsyncCallback<T> callback) {
		if (remoteAvailable()) {
			callback = decorateWithPostInsert(callback);
			remote.get(className, id, callback);
		} else if (localAvailable()) {
			local.get(className, id, callback);
		}
	}

	@Override
	public <T extends Persistable> void get(String className, List<Long> ids,
			AsyncCallback<List<T>> callback) {
		if (remoteAvailable()) {
			if (localAvailable()) {
				callback = new CallbackProxy<List<T>>(callback) {
					@Override
					protected void afterSuccess(List<T> result) {
						for (T t : result) {
							localInsertOrUpdate(t, new NullCallback<T>());
						}
					}
				};
			}
			remote.get(className, ids, callback);
		} else if (localAvailable()) {
			local.get(className, ids, callback);
		}
	}

	@Override
	public <T extends Persistable> void find(String className, Query query,
			AsyncCallback<List<T>> callback) {
		if (remoteAvailable()) {
			if (localAvailable()) {
				callback = new CallbackProxy<List<T>>(callback) {
					@Override
					protected void afterSuccess(List<T> result) {
						for (T t : result) {
							localInsertOrUpdate(t, new NullCallback<T>());
						}
					}
				};
			}
			remote.find(className, query, callback);
		} else if (localAvailable()) {
			local.find(className, query, callback);
		}
	}

	@Override
	public <T extends Persistable> void delete(T t, AsyncCallback<Void> callback) {
		if (localAvailable())
			local.delete(t, callback);
		if (remoteAvailable())
			remote.delete(t, callback);
		else {
			// DBLog log = new DBLog(entity.getEntityName(), entity.getId(t),
			// DELETE);
			// local.insert(log, new NullCallback<DBLog>());
		}
	}
}
